package com.evon.yardmanagement.dao;

import java.io.Serializable;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;
import org.hibernate.FlushMode;
import org.hibernate.HibernateException;
import org.hibernate.Interceptor;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.AnnotationConfiguration;
import org.hibernate.cfg.Configuration;
import org.hibernate.cfg.Environment;
import org.hibernate.cfg.NamingStrategy;
import org.hibernate.context.internal.ManagedSessionContext;

public class HibernateHelper {
	/**
	 * The maximum number of elements that can be in a single in clause. This is
	 * due to bug
	 * http://opensource.atlassian.com/projects/hibernate/browse/HHH-2166
	 */
	public static final int MAX_IN_CLAUSE_LENGTH = 500;

	private static final Logger LOG = Logger.getLogger(HibernateHelper.class);

	private Configuration configuration;
	private SessionFactory sessionFactory;
	private final NamingStrategy namingStrategy;
	private final Interceptor interceptor;

	private static String CONFIG_FILE_LOCATION = "com/evon/yardmanagement/dao/hibernate.cfg.xml";
	private static String configFile = CONFIG_FILE_LOCATION;

	/**
	 * Default constructor.
	 */
	public HibernateHelper() {
		this(null, null);
	}

	/**
	 * Constructor to build the hibernate helper.
	 * 
	 * @param namingStrategy
	 *            the name strategy, if one is needed, null otherwise.
	 * @param interceptor
	 *            the interceptor, if one is needed, null otherwise.
	 */
	public HibernateHelper(NamingStrategy namingStrategy,
			Interceptor interceptor) {
		this.namingStrategy = namingStrategy;
		this.interceptor = interceptor;
	}

	/**
	 * This builds both the configuration and the session factory.
	 */
	public synchronized void initialize() {
		if (configuration == null) {
			try {
				configuration = new AnnotationConfiguration();
				initializeConfig();

				// We call buildSessionFactory twice, because it appears that
				// the annotated classes are
				// not 'activated' in the config until we build. The filters
				// required the classes to
				// be present, so we throw away the first factory and use the
				// second. If this is
				// removed, you'll likely see a NoClassDefFoundError in the unit
				// tests
				SessionFactory sf = configuration.buildSessionFactory();
				sf.close();

				modifyConfig();

				buildSessionFactory();
			} catch (HibernateException e) {
				LOG.error(e.getMessage(), e);
				throw new ExceptionInInitializerError(e);
			}
		}
	}

	/**
	 * This method is used to edit the configuration object and call
	 * configuration.configure. This is called by the initialize method after
	 * creating an instance of AnnotationConfiguration. The last thing this
	 * method should do is to call configuration.configure.
	 */
	protected void initializeConfig() {
		if (namingStrategy != null) {
			configuration.setNamingStrategy(namingStrategy);
		}
		if (interceptor != null) {
			configuration.setInterceptor(interceptor);
		}

		configuration = configuration.configure(configFile);
	}

	/**
	 * This method is used to modify the configuration after
	 * configuration.config has been called. This is called by the initialize
	 * method creates an instance of AnnotationConfiguration, calls
	 * initializeConfig and creates and destroys a session factory in order to
	 * work around a bug in the configuration that causes some classes to not be
	 * activated in the config properly.
	 */
	protected void modifyConfig() {
		// do nothing
	}

	/**
	 * This method just creates the session factory using the current config.
	 */
	protected void buildSessionFactory() {
		if (sessionFactory != null) {
			sessionFactory.close();
		}
		sessionFactory = configuration.buildSessionFactory();
	}

	/**
	 * @return the configuration
	 */
	public Configuration getConfiguration() {
		return configuration;
	}

	/**
	 * @return the sessionFactory
	 */
	public SessionFactory getSessionFactory() {
		return sessionFactory;
	}

	/**
	 * Get the session that is bound to the current context.
	 * 
	 * @return the current session
	 */
	public Session getCurrentSession() {
		return getSessionFactory().getCurrentSession();
	}

	/**
	 * Starts a transaction on the current Hibernate session. Intended for use
	 * in unit tests - DAO / Service layer logic should rely on
	 * container-managed transactions
	 * 
	 * @return a Hibernate session.
	 */
	public Transaction beginTransaction() {
		return getSessionFactory().getCurrentSession().beginTransaction();
	}

	/**
	 * Checks if the transaction is active and then rolls it back.
	 * 
	 * @param tx
	 *            the Transaction to roll back.
	 */
	public void rollbackTransaction(Transaction tx) {
		if (tx != null && tx.isActive()) {
			tx.rollback();
		}
	}

	/**
	 * Open a hibernate session and bind it as the current session via
	 * {@link ManagedSessionContext#bind(org.hibernate.classic.Session)}. The
	 * hibernate property "hibernate.current_session_context_class" must be set
	 * to "managed" for this to have effect. This method should be called from
	 * within an Interceptor or Filter type class that is setting up the scope
	 * of the Session. This method should then call
	 * {@link HibernateUtil#unbindAndCleanupSession()} when the scope of the
	 * Session is expired.
	 * 
	 * @see ManagedSessionContext#bind(org.hibernate.classic.Session)
	 */
	public void openAndBindSession() {
		openAndBindSession(getSessionFactory());
	}

	/**
	 * Open a hibernate session and bind it as the current session via
	 * {@link ManagedSessionContext#bind(org.hibernate.classic.Session)}. The
	 * hibernate property "hibernate.current_session_context_class" must be set
	 * to "managed" for this to have effect This method should be called from
	 * within an Interceptor or Filter type class that is setting up the scope
	 * of the Session. This method should then call
	 * {@link HibernateUtil#unbindAndCleanupSession()} when the scope of the
	 * Session is expired.
	 * 
	 * @see ManagedSessionContext#bind(org.hibernate.classic.Session)
	 * @param sf
	 *            the session factory.
	 */
	public void openAndBindSession(SessionFactory sf) {
		SessionFactory sessionFactoryImplementor = (SessionFactory) sf;
		org.hibernate.Session currentSession = sessionFactoryImplementor
				.openSession();
		currentSession.setFlushMode(FlushMode.COMMIT);
		ManagedSessionContext.bind(currentSession);
	}

	/**
	 * Close the current session and unbind it via
	 * {@link ManagedSessionContext#unbind(SessionFactory)}. The hibernate
	 * property "hibernate.current_session_context_class" must be set to
	 * "managed" for this to have effect. This method should be called from
	 * within an Interceptor or Filter type class that is setting up the scope
	 * of the Session, when this scope is about to expire.
	 */
	public void unbindAndCleanupSession() {
		unbindAndCleanupSession(getSessionFactory());
	}

	/**
	 * Close the current session and unbind it via
	 * {@link ManagedSessionContext#unbind(SessionFactory)}. The hibernate
	 * property "hibernate.current_session_context_class" must be set to
	 * "managed" for this to have effect. This method should be called from
	 * within an Interceptor or Filter type class that is setting up the scope
	 * of the Session, when this scope is about to expire.
	 * 
	 * @param sf
	 *            the session factory.
	 */
	public void unbindAndCleanupSession(SessionFactory sf) {
		org.hibernate.Session currentSession = ManagedSessionContext.unbind(sf);
		if (currentSession != null) {
			currentSession.flush();
			//currentSession.getTransaction().commit();
			currentSession.close();
		}
	}

	/**
	 * Determines if we are currently using managed sessions.
	 * 
	 * @return true if we are, false otherwise.
	 */
	public boolean isManagedSession() {
		return "managed".equals(getConfiguration().getProperty(
				Environment.CURRENT_SESSION_CONTEXT_CLASS));
	}

	/**
	 * Break up a list of items into separate in clauses, to avoid limits
	 * imposed by databases or by Hibernate bug
	 * http://opensource.atlassian.com/projects/hibernate/browse/HHH-2166.
	 * 
	 * @param items
	 *            list of items to include in the in clause
	 * @param columnName
	 *            name of column to match against the list
	 * @param blocks
	 *            empty Map of HQL param name to param list of values to be set
	 *            in the HQL query - it will be populated by the method
	 * @return full HQL "in" clause, of the form:
	 *         " columnName in (:block1) or ... or columnName in (:blockN)"
	 */
	public static String buildInClause(List<? extends Serializable> items,
			String columnName, Map<String, List<? extends Serializable>> blocks) {
		StringBuffer inClause = new StringBuffer();
		for (int i = 0; i < items.size(); i += MAX_IN_CLAUSE_LENGTH) {
			List<? extends Serializable> block = items.subList(i, Math.min(
					items.size(), i + MAX_IN_CLAUSE_LENGTH));
			String paramName = "block" + i / MAX_IN_CLAUSE_LENGTH;
			if (inClause.length() > 0) {
				inClause.append(" or");
			}
			inClause.append(" " + columnName + " in (:" + paramName + ")");
			blocks.put(paramName, block);
		}
		return inClause.toString();
	}

	/**
	 * Bind the parameters returned by {@link #buildInClause(List, String, Map)}
	 * to a hibernate Query.
	 * 
	 * @param query
	 *            hibernate query to bind to
	 * @param blocks
	 *            blocks to be bound to query
	 */
	public static void bindInClauseParameters(Query query,
			Map<String, List<? extends Serializable>> blocks) {
		for (Map.Entry<String, List<? extends Serializable>> block : blocks
				.entrySet()) {
			query.setParameterList(block.getKey(), block.getValue());
		}
	}

	// /**
	// * Get the class validator for a given object, caching validators to speed
	// * up subsequent lookups.
	// *
	// * @param <T>
	// * type of object to retrieve the ClassValidator for
	// * @param o
	// * object to retrieve the ClassValidator for
	// * @return the ClassValidator for the given object
	// */
	// @SuppressWarnings( { "unchecked", "rawtypes" })
	// public static synchronized <T> ClassValidator<T> getClassValidator(T o) {
	// ClassValidator<T> classValidator = (ClassValidator<T>)
	// CLASS_VALIDATOR_MAP
	// .get(o.getClass());
	// if (classValidator == null) {
	// classValidator = new ClassValidator(o.getClass(), bundle,
	// INTERPOLATOR, null, null);
	// CLASS_VALIDATOR_MAP.put(o.getClass(), classValidator);
	// }
	// return classValidator;
	// }
	//
	// /**
	// * @param aResourceBundle
	// * the bundle to set
	// */
	// public void setBundle(ResourceBundle aResourceBundle) {
	// if (aResourceBundle == null) {
	// bundle = ResourceBundle.getBundle(DEFAULT_BUNDLE);
	// } else {
	// bundle = aResourceBundle;
	// }
	// INTERPOLATOR.setBundle(bundle);
	// }
	//
	// /**
	// * Set name of the resource bundle to be used by validator.
	// *
	// * @param bundleName
	// * the name of the bundle. If null or empty, the default bundle
	// * is used.
	// */
	// public void setBundleName(String bundleName) {
	// bundle = ResourceBundle.getBundle(StringUtils.defaultIfEmpty(
	// bundleName, DEFAULT_BUNDLE));
	// INTERPOLATOR.setBundle(bundle);
	// }
	//
	// /**
	// * @param entity
	// * the entity to validate
	// * @return a map of validation messages keyed by the property path. The
	// keys
	// * represent the field/property validation errors however, when key
	// * is null it means the validation is a type/class validation error
	// */
	// public static Map<String, String[]> validate(PersistentObject entity) {
	// Map<String, List<String>> messageMap = new HashMap<String,
	// List<String>>();
	// ClassValidator<PersistentObject> classValidator =
	// getClassValidator(entity);
	// InvalidValue[] validationMessages = classValidator
	// .getInvalidValues(entity);
	// for (InvalidValue validationMessage : validationMessages) {
	// String path = StringUtils.defaultString(validationMessage
	// .getPropertyPath());
	// List<String> m = messageMap.get(path);
	// if (m == null) {
	// m = new ArrayList<String>();
	// messageMap.put(path, m);
	// }
	// String msg = validationMessage.getMessage();
	// msg = msg.replace("(fieldName)", "").trim();
	// m.add(msg);
	// }
	//
	// return convertMapListToMapArray(messageMap);
	// }
	//
	// /**
	// * Convert list to array for map of string, string list.
	// *
	// * @param messageMap
	// * map of string, string list to convert.
	// * @return map string, string list.
	// */
	// public static Map<String, String[]> convertMapListToMapArray(
	// Map<String, List<String>> messageMap) {
	// Map<String, String[]> returnMap = new HashMap<String, String[]>();
	// for (Map.Entry<String, List<String>> entry : messageMap.entrySet()) {
	// returnMap.put(entry.getKey(), entry.getValue().toArray(
	// new String[entry.getValue().size()]));
	// }
	// return returnMap;
	// }

}
